Repository navigation
Conversation
|
please bisect which commit broke this |
|
Bisected to 7a8c43cf265f16625a9d626d1fa6637bf505d9fa, which introduced Its parent |
66352a3 to
8a2ca96
Compare
|
Rebased onto current 2.3.x to resolve the Turbo pin conflict; the implementation is unchanged. Full suites pass with and without Turbo (22,459 tests each), and the rebuilt native version and parity checks pass. New-head CI is running. |
ondrejmirtes
left a comment
There was a problem hiding this comment.
The returned new ObjectType(Closure::class) is not good enough. Because it ends up in templateArgumentObserver->collectArgument in processArgs, it influences bidirectional type narrowing results.
Here's a failing test on top of your branch. Add it to your code and fix that.
|
Thanks! The crash is fixed for the reported snippet, but I don't think returning 1. Other first-class callables still crash on this branch
/**
* @template T
* @param array{producer: callable(): T, consumer: callable(T): void} $pair
*/
function pipe(array $pair): void {}
function a(\Closure $f): void
{
pipe(['producer' => $f(...), 'consumer' => function ($v): void {}]); // Internal error
pipe(['producer' => 'strlen'(...), 'consumer' => function ($v): void {}]); // Internal error
}2. A method callable gives a less precise type than its equivalent forms The skeleton exists to type the closures nested in the array. A plain class Producer
{
public function produce(): int { return 1; }
public static function produceStatic(): int { return 1; }
}
function test(Producer $p): void
{
pipe(['producer' => $p->produce(...), 'consumer' => function ($v): void {
\PHPStan\dumpType($v); // mixed
}]);
$callable = $p->produce(...);
pipe(['producer' => $callable, 'consumer' => function ($v): void {
\PHPStan\dumpType($v); // int
}]);
pipe(['producer' => Producer::produceStatic(...), 'consumer' => function ($v): void {
\PHPStan\dumpType($v); // int
}]);
}I'd fix this in
3. Not caused by this PR, but it's in the same code: the skeleton reads stale scope state Leaves are priced from the scope before the array is evaluated. A sibling that changes state earlier in the array therefore gives the nested closure a wrong type, not just a less precise one. This is a false positive in 2.3.x that 2.2.x doesn't report: /**
* @template T
* @param array{first: mixed, value: T, callback: callable(T): void} $spec
*/
function run(array $spec): void {}
function fp(): void
{
$i = 0;
run([
'first' => $i++,
'value' => $i, // skeleton prices this as 0, at runtime it's 1
'callback' => function ($v): void {
if ($v === 1) { // "Strict comparison using === between 0 and 1 will always evaluate to false."
echo 'one';
}
},
]);
}The same happens with Results are the same with and without bleeding edge. The skeleton never reaches |
|
Thanks for the detailed examples. I moved the fix into the array skeleton and removed the initializer fallback. Scope-known method callables now retain their variants, while walk-dependent callable expressions use mixed without an eager receiver walk. The skeleton also stops trusting pre-array state after potentially state-changing keys or values, including nested arrays. The new type regressions pass with and without bleeding edge; full PHP and Turbo suites pass (22,476 tests each), including the existing incompatible-callable control. New-head CI is running. |
Fixes phpstan/phpstan#15432.
Generic array arguments containing first-class callables and closures can crash while PHPStan constructs the preliminary array type. Returning an unparameterized
Closurefrom the initializer resolver also loses the return type used to infer a sibling closure's parameter.Handle this in
ArgumentsHandler::gatherArrayArgTypeSkeleton(), without evaluating receivers or argument expressions. Scope-known instance method callables retain their method variants throughcreateFirstClassCallable(). Expressions that require a walk usemixed, including anonymous receivers and expression-named function or static callables. Remove the initializer fallback from the earlier revision and mirror the skeleton fix in Turbo.The skeleton also stops reusing pre-array scope state after a potentially state-changing key or value. Keys are priced before values, including nested arrays; closure bodies are not executed. Later constants remain precise. This prevents incorrect nested closure types after assignments, increments or impure/by-reference calls. The real argument walk still replaces the skeleton before template argument observation.
Regression coverage includes the original anonymous receiver, expression-named callables, method/hoisted/static equivalence, dynamic callable names, assignments, increments, nested and key effects, by-reference calls, and stable/deferred-body controls. Existing incompatible-callable diagnostics are retained. The type assertions run with and without bleeding edge.
Local validation:
Cross-platform, sanitizer and downstream checks remain for the new-head upstream CI; pending checks are not local test results.